﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal abstract class GroupCacheEnumeratorBase : PeekableCacheEnumerator
  {
    #region Constructor

    protected GroupCacheEnumeratorBase(
      IAsyncEnumerator<DataPath> childEnumerator,
      IContextualCompareHelper compareHelper,
      IContextualItemHelper itemHelper,
      IContextualGroupHelper groupHelper,
      int cacheSize )
      : base( childEnumerator, compareHelper, itemHelper, cacheSize )
    {
      if( groupHelper == null )
        throw new ArgumentNullException( "groupHelper" );

      m_groupHelper = groupHelper;
    }

    #endregion

    #region GroupHelper Protected Property

    protected IContextualGroupHelper GroupHelper
    {
      get
      {
        return m_groupHelper;
      }
    }

    private readonly IContextualGroupHelper m_groupHelper;

    #endregion

    protected virtual void PathExpanded( DataPath path )
    {
      IList<DataPath> cache = this.CachedItems;
      int index = cache.IndexOf( path );

      IEnumerable<DataPath> verifiedItems = ( index < 0 ) ? cache : cache.Take( index + 1 );

      Debug.Assert(
        verifiedItems.All( ( item ) => !path.IsAncestorOf( item ) ),
        "No expanded children of the specified expanding path is supposed to be found" );

      if( index >= 0 )
      {
        this.FlushCache( index, false );
        this.InvalidateCountInformation( true );
      }
      else
      {
        this.InvalidateCountInformation( false );
      }
    }

    protected void PathCollapsed( DataPath path )
    {
      bool found = false;
      int count = this.CachedItems.Count;

      for( int i = 0; i < count; i++ )
      {
        if( !path.IsAncestorOf( this.CachedItems[ i ] ) )
          continue;

        //I use a copy instead of "e.Path" because i don't want the e.Path instance to be "modified" by
        //internal mecanics. This was not the source of a bug, it is just for precaution
        this.CachedItems[ i ] = this.CachedItems[ i ].CreateAncestorPath( path.Depth );

        int next = i + 1;
        if( next < count )
        {
          this.FlushCache( next, false );
        }

        found = true;
        break;
      }

      this.InvalidateCountInformation( found );
    }

    protected override void ChildDataItemChanged( ItemChangedInfo itemChangedInfo )
    {
      if( itemChangedInfo == null )
        throw new ArgumentNullException( "itemChangedInfo" );

      DataSourceContext context = this.GetCurrentContext();

      int groupCount = context.GroupContext.GroupDescriptions.Count;

      //If no grouping exist, just do the base job.
      if( groupCount == 0 )
      {
        base.ChildDataItemChanged( itemChangedInfo );
        return;
      }

      DataPath newCompletePath = ( DataPath )itemChangedInfo.Target;

      if( newCompletePath.Depth <= groupCount )
        throw new DataInternalException();

      int cachedPathIndex = this.GetModifiedItemIndex( newCompletePath );
      DataPath cachedPath = ( cachedPathIndex != -1 ) ? this.CachedItems[ cachedPathIndex ] : null;
      DataPath oldPath = this.ReCreateOldPath( context, itemChangedInfo, cachedPath );

      if( oldPath.Depth >= groupCount
        && DataPath.AreSameItems( oldPath.Path, newCompletePath.Path, groupCount ) )
      {
        //No group property affected.
        base.ChildDataItemChanged( itemChangedInfo );
        return;
      }


      //This Call assume the code that have been handled in GroupEnumerator.
      //Any cases expected to be handled in the GroupEnumerator will throw an exception.
      if( oldPath.Depth >= groupCount )
        throw new DataInternalException();

      int collapsedDepth = context.GroupContext.GroupCollections.GetCollapsedDepth( newCompletePath );

      //haveOldItem: it was "maybe" part of a collapsed group. can't know for 
      //sure since we do not have the old value. Set is a unknown: "null".
      Nullable<bool> haveOldItem = null;

      //oldItemIndex: We do not have any old value, can only know if it is "before"
      // or "after" the first cache item.
      Nullable<int> oldItemIndex = ( cachedPathIndex != -1 )
        ? cachedPathIndex
        : this.GetIncompletePathIndex( context, oldPath );

      //haveNewItem: We have a new item only if it is not part of a collapsed group.
      Nullable<bool> haveNewItem = ( collapsedDepth == 0 );

      //newItemIndex: At witch place the new item is expected to be inserted ?
      Nullable<int> newItemIndex = ( haveNewItem.Value )
        ? this.GetInsertIndex( context, newCompletePath )
        : ( Nullable<int> )null;

      //Handle the changes.
      this.HandleItemChangeCore( haveOldItem, oldItemIndex, haveNewItem, newItemIndex, newCompletePath );
    }

    protected override void ChildDataChanged( DataChangedEventArgs args )
    {

      DataPath newItem = ( DataPath )args.NewItem;
      DataPath oldItem = ( DataPath )args.OldItem;

      DataPath newItemToAdd;
      if( args.Action == DataChangedAction.Add
        && this.HandleDataAdded( newItem, out newItemToAdd ) )
      {
        if( newItemToAdd != null )
        {
          this.RaiseDataAdd( newItemToAdd );
        }
      }
      else if( args.Action == DataChangedAction.Replace
        && this.HandleDataAdded( newItem, out newItemToAdd ) )
      {
        this.ChildDataRemoved( oldItem, null );

        if( newItemToAdd != null )
        {
          this.RaiseDataReplaced( oldItem, newItemToAdd );
        }
        else
        {
          this.RaiseDataRemoved( oldItem, null, null );
        }
      }
      else
      {
        //Handle as usual
        base.ChildDataChanged( args );
      }
    }

    protected override void ChildDataRemoved( DataPath oldItem, SourcePosition removePosition )
    {
      if( oldItem == null )
        throw new ArgumentNullException( "oldItem" );

      if( this.CachedItems.Count == 0 )
      {
        base.ChildDataRemoved( oldItem, removePosition );
        return;
      }

      DataSourceContext context = this.GetCurrentContext();
      int collapsedDepth = context.GroupContext.GroupCollections.GetCollapsedDepth( oldItem );

      if( collapsedDepth != 0 && collapsedDepth < oldItem.Depth )
      {
        DataPath collapsedParent = oldItem.CreateAncestorPath( collapsedDepth );

        Nullable<int> removeIndex = this.GetRemoveIndex( context, collapsedParent );
        if( removeIndex.Value >= 0 && removeIndex.Value < this.CachedItems.Count )
        {

          //We can't know for sure if this collapsed group is still existing. Clear it from
          //the cache to ensure consistency.
          this.FlushCache( removeIndex.Value, false );

          removeIndex = int.MaxValue;
        }

        //This call will update the counts appropriatly.
        this.HandleItemChangeCore( null, removeIndex, false, null, null );
      }
      else
      {
        base.ChildDataRemoved( oldItem, removePosition );
      }

      if( context.GroupContext.AnyHeaderFooter )
      {

        int levelDiff = -1;
        for( int i = 0; i < this.CachedItems.Count; i++ )
        {
          DataPath currentItem = this.CachedItems[ i ];

          for( int j = 0; j < currentItem.Depth && j < oldItem.Depth; j++ )
          {
            if( !object.Equals( currentItem[ j ], oldItem[ j ] ) )
            {
              if( !( currentItem[ j ] is HeaderFooterDataItem ) )
              {
                levelDiff = Math.Max( j, levelDiff );
              }
              break;
            }
          }
        }

        if( levelDiff < oldItem.Depth )
        {
          for( int i = 0; i < this.CachedItems.Count; i++ )
          {
            DataPath currentItem = this.CachedItems[ i ];

            for( int j = 0; j < currentItem.Depth && j < oldItem.Depth; j++ )
            {
              if( !object.Equals( currentItem[ j ], oldItem[ j ] ) )
              {
                if( currentItem[ j ] is HeaderFooterDataItem )
                {
                  if( levelDiff < j )
                  {
                    this.CachedItems.RemoveAt( i );
                    i--;
                  }
                }
                break;
              }
            }
          }
        }


        this.InvalidateCountInformation();
      }
    }

    internal virtual void ExpansionChanging( ExpansionAction action, DataPath path )
    {
      switch( action )
      {
        case ExpansionAction.ExpandAll:
        case ExpansionAction.CollapseAll:
          this.FlushCache();
          break;

        case ExpansionAction.Expand:
          this.PathExpanded( path );
          break;

        case ExpansionAction.Collapse:
          this.PathCollapsed( path );
          break;

        default:
          break;
      }
    }

    internal override int GetModifiedItemIndex( DataPath item )
    {
      if( item == null )
        throw new ArgumentNullException( "item" );

      if( ( item.LastChild is HeaderFooterDataItem )
        || ( item.LastChild is GroupDataItem ) )
        throw new ArgumentException( "item is supposed to be a user object" );

      int itemIndex = -1;

      for( int i = 0; i < this.CachedItems.Count; i++ )
      {
        DataPath path = this.CachedItems[ i ];
        if( path.Depth == item.Depth )
        {
          if( object.Equals( path.LastChild, item.LastChild ) )
          {
            bool equal = true;

            for( int j = 0; j < path.Depth - 1; j++ )
            {
              if( !object.Equals( path[ j ], item[ j ] )
                && ( !( path[ j ] is GroupDataItem )
                  || !( item[ j ] is GroupDataItem ) ) )
              {
                equal = false;
                break;
              }
            }

            if( equal )
            {
              itemIndex = i;
              break;
            }
          }
        }
      }

      return itemIndex;
    }

    internal DataPath ProvideCachedPath( DataSourceContext context, DataPath newPath )
    {
      if( newPath == null )
        throw new ArgumentNullException( "newPath" );

      lock( this.SyncRoot )
      {
        if( this.MatchCurrentContext( context ) )
        {
          int itemIndex = this.GetModifiedItemIndex( newPath );

          if( itemIndex >= 0 )
            return this.CachedItems[ itemIndex ];
        }
      }

      return null;
    }

    private bool HandleDataAdded( DataPath newItem, out DataPath newItemToRaise )
    {
      if( newItem == null )
        throw new ArgumentNullException( "newItem" );

      newItemToRaise = null;

      if( this.CachedItems.Count == 0 )
        return false;

      bool handled = false;
      DataSourceContext context = this.GetCurrentContext();
      int collapsedDepth = context.GroupContext.GroupCollections.GetCollapsedDepth( newItem );
      bool anyHeaderFooter = context.GroupContext.AnyHeaderFooter;
      DataPath addedItem = null;

      if( collapsedDepth != 0 && collapsedDepth < newItem.Depth )
      {
        DataPath collapsedParent = newItem.CreateAncestorPath( collapsedDepth );

        //Ok it means that there "may" be an Collapsed group to add. 
        //If its is already in cache, there is clearly no item to add...
        if( this.CachedItems.IndexOf( collapsedParent ) == -1 )
        {
          Nullable<bool> haveAnItemToAddForSure = null;
          int insertIndex = this.GetInsertIndex( context, collapsedParent );

          //If "haveAnItemToAddForSure" is true, this tells us that the item was not in cache but
          //should have been. There is clearly an item to add, which is the collapsedParent.
          if( insertIndex >= 0 && insertIndex <= this.CachedItems.Count )
          {
            haveAnItemToAddForSure = true;
            addedItem = collapsedParent;
          }

          this.HandleItemChangeCore( false, null, haveAnItemToAddForSure, insertIndex, collapsedParent );
          newItemToRaise = ( haveAnItemToAddForSure.GetValueOrDefault( false ) ) ? collapsedParent : newItem;
        }

        handled = true;
      }
      else if( anyHeaderFooter )
      {
        base.ChildDataAdded( newItem );
        addedItem = newItem;
        newItemToRaise = newItem;
        handled = true;
      }

      if( anyHeaderFooter && addedItem != null )
      {
        this.AddMissingHeadersFootersToCache( false, addedItem );
        this.AddMissingHeadersFootersToCache( true, addedItem );

        this.InvalidateCountInformation();
      }

      return handled;
    }

    private void AddMissingHeadersFootersToCache( bool addHeaders, DataPath addedItem )
    {
      int newItemIndex = this.CachedItems.IndexOf( addedItem );

      if( newItemIndex == -1 )
        return;

      int missingHeadersFootersLevel = -1;

      int cacheLimit = ( addHeaders ) ? 0 : this.CachedItems.Count - 1;
      if( newItemIndex == cacheLimit )
      {
        bool isAtLimit = ( addHeaders ) ? this.IsCacheAtStart : this.IsCacheAtEnd;
        if( !isAtLimit )
        {
          missingHeadersFootersLevel = addedItem.Depth;
        }
      }
      else
      {

        int adjacentIncrement = ( addHeaders ) ? -1 : 1;
        DataPath adjacentItem = this.CachedItems[ newItemIndex + adjacentIncrement ];

        missingHeadersFootersLevel = adjacentItem.Depth - 1;

        Debug.Assert( adjacentItem.Depth >= 0 );

        for( int i = 0; i < adjacentItem.Depth && i < addedItem.Depth; i++ )
        {
          if( !object.Equals( adjacentItem[ i ], addedItem[ i ] ) )
          {
            missingHeadersFootersLevel = i;
            break;
          }
        }
      }

      DataSourceContext context = this.GetCurrentContext();
      List<DataPath> headersFooters = new List<DataPath>();
      bool flushRemaining = false;


      for( int i = addedItem.Depth - 2; i >= missingHeadersFootersLevel; i-- )
      {
        if( i == -1 )
          continue;

        DataPath levelPath = addedItem.CreateAncestorPath( i + 1 );

        Debug.Assert( m_groupHelper.IsGroupPath( context, levelPath ) );

        IAsyncEnumerator<DataPath> headersFootersEnumerator = ( addHeaders )
          ? context.GroupContext.GetHeadersEnumerator( levelPath )
          : context.GroupContext.GetFootersEnumerator( levelPath );

        if( !headersFootersEnumerator.IsFullySynchronized )
        {
          flushRemaining = true;
          break;
        }
        else
        {
          ISyncEnumerator<DataPath> syncFootersEnumerator = new AsyncToSyncEnumerator<DataPath>( headersFootersEnumerator );
          if( addHeaders )
          {
            headersFooters.InsertRange( 0, syncFootersEnumerator.FetchAll( false ) );
          }
          else
          {
            headersFooters.AddRange( syncFootersEnumerator.FetchAll( false ) );
          }
        }
      }


      if( flushRemaining )
      {
        if( addHeaders )
        {
          if( newItemIndex > 0 )
          {
            this.FlushCache( newItemIndex - 1, true );
          }
          newItemIndex = 0;
        }
        else
        {
          this.FlushCache( newItemIndex + 1, false );
        }
      }

      if( addHeaders )
      {
        this.CachedItems.InsertRange( newItemIndex, headersFooters );
        newItemIndex += headersFooters.Count;
      }
      else
      {
        this.CachedItems.InsertRange( newItemIndex + 1, headersFooters );
      }
    }

    private Nullable<int> GetIncompletePathIndex( DataSourceContext context, DataPath item )
    {
      if( item == null )
        throw new ArgumentNullException( "item" );

      IList<GroupDescription> grouping = context.GroupContext.GroupDescriptions;
      DataPath firstCached = this.CachedItems[ 0 ];

      Nullable<int> index = null;
      for( int i = 0; i < grouping.Count; i++ )
      {
        object itemValue;
        object firstCachedValue;
        string propertyPath = grouping[ i ].GetPropertyName();

        if( !this.ItemHelper.TryGetMemberValue( context, propertyPath, item, out itemValue )
          || !this.ItemHelper.TryGetMemberValue( context, propertyPath, firstCached, out firstCachedValue ) )
          break;

        index = this.CompareHelper.Compare( context, propertyPath, item, firstCached );

        if( index < 0 )
        {
          index = int.MinValue;
          break;
        }
        else if( index > 0 )
        {
          index = int.MaxValue;
          break;
        }
        else
        {
          index = null;
        }
      }

      return index;
    }

    private DataPath ReCreateOldPath( DataSourceContext context, ItemChangedInfo itemChangedInfo, DataPath cachedPath )
    {
      // Try to retrieve the path from cache.
      if( cachedPath != null )
        return cachedPath;

      DataPath targetPath = ( DataPath )itemChangedInfo.Target;
      GroupDescription[] groupDescriptions = context.GroupContext.GroupDescriptions.ToArray();

      // The change has no impact on the grouping.
      if( !groupDescriptions.Any() || groupDescriptions.All( ( group ) => !itemChangedInfo.GetIsValueChanged( group ) ) )
        return targetPath;

      DataItem[] dataItems = new DataItem[ targetPath.Depth ];
      int length = 0;

      for( int i = 0; i < groupDescriptions.Length; i++ )
      {
        GroupDescription group = groupDescriptions[ i ];
        object oldValue;

        if( itemChangedInfo.GetIsValueChanged( group ) )
        {
          if( !itemChangedInfo.TryGetOldValue( group, out oldValue ) )
            break;
        }
        else
        {
          oldValue = targetPath[ i ].Data;
        }

        dataItems[ i ] = new GroupDataItem( oldValue );
        length = i + 1;
      }

      if( ( length == groupDescriptions.Length ) && ( length == targetPath.Depth + 1 ) )
      {
        dataItems[ length ] = targetPath[ length ];
        length++;
      }

      return new DataPath( dataItems, length );
    }
  }
}
